本文同步刊載於 「為你自己學 Python - 虛擬機器五部曲(五)」
上個章節介紹了 LEGB 四種 Scope 的設計,但在這過程中有看到一個特別的物件叫做 Cell Object,在 Python 這個物件是用來實現「閉包(Closure)」效果的。不少程式語言都有「閉包(Closure)」這個的設計,在「為你自己學 Python」的函數 - 進階篇有介紹過,不過這個章節要直接從 CPython 的原始碼來看看 Cell Object 是怎麼回事,以及閉包是怎麼設計的。
先看看 Cell 的結構:
// 檔案:Include/cpython/cellobject.h
typedef struct {
PyObject_HEAD
PyObject *ob_ref;
} PyCellObject;
跟其它型態相比,PyCellObject
的結構單純很多,除了標準的 PyObject_HEAD
之外就只有一個 ob_ref
成員,這個 ob_ref
是個 PyObject
型別,所以這讓 PyCellObject
可以儲存對任何一種 Python 的物件的指標。來看看它是怎麼建立的,以這段程式碼為例:
def hi():
a = 1
b = 2
def hey():
print(a)
先來看看定義 hi()
函數的部份 Bytecode:
// ... 略 ...
0 MAKE_CELL 2 (a)
2 4 LOAD_CONST 1 (1)
6 STORE_DEREF 2 (a)
3 8 LOAD_CONST 2 (2)
10 STORE_FAST 0 (b)
// ... 略 ...
在正式建立 hey()
函數之前,有好幾個沒看過的新指令,我們一行一行來看,首先看看 MAKE_CELL 2
:
// 檔案:Python/bytecodes.c
inst(MAKE_CELL, (--)) {
PyObject *initial = GETLOCAL(oparg);
PyObject *cell = PyCell_New(initial);
if (cell == NULL) {
goto resume_with_error;
}
SETLOCAL(oparg, cell);
}
GETLOCAL(oparg)
在上個章節看過,這會根據 oparg
的值從目前的 Frame 的 localsplus
這個陣列上取值,接著把它傳給 PyCell_New()
函數:
// 檔案:Objects/cellobject.c
PyObject *
PyCell_New(PyObject *obj)
{
PyCellObject *op;
op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
if (op == NULL)
return NULL;
op->ob_ref = Py_XNewRef(obj);
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}
在這個函數建立了一個 PyCellObject
物件,然後把傳進來的 obj
設定給了這個 Cell 的 ob_ref
成員。然後再把這個 Cell 透過 SETLOCAL(oparg, cell)
巨集擺回區域變數,也就是 Frame 的 localsplus
陣列的指定位置。也就是說,還沒做到 a = 1
的設定之前,已經在區域變數 localsplus
陣列裡幫變數 a
準備好一個 Cell 了。
接下來的 STORE_DEREF 2
我們在上個章節就看過了,這裡的 2
跟剛剛的 MAKE_CELL 2
也就是 oparg
是一樣的,表示是把值存放在剛剛準備好的 Cell 裡。接下來的變數 b
就沒這待遇了,它就只是一般的區域變數,所以這裡就是用 STORE_FAST 0
來處理。
再接著往下看:
// ... 略 ...
5 12 LOAD_CLOSURE 2 (a)
14 BUILD_TUPLE 1
16 LOAD_CONST 3 (<code object)
18 MAKE_FUNCTION 8 (closure)
20 STORE_FAST 1 (hey)
22 RETURN_CONST 0 (None)
// ... 略 ...
從編譯出來的 Bytecode 看的出來,雖然對於內層的 hey()
函數也是用我們之前看過的 MAKE_FUNCTION
指令,但這之前有個 LOAD_CLOSURE 2
指令,這是在做什麼?
// 檔案:Python/bytecodes.c
inst(LOAD_CLOSURE, (-- value)) {
value = GETLOCAL(oparg);
ERROR_IF(value == NULL, unbound_local_error);
Py_INCREF(value);
}
其實沒做什麼事,就只是把 Frame 的 localsplus
陣列的值讀出來而已。再來的 BUILD_TUPLE 1
是在做什麼?
// 檔案:Python/bytecodes.c
inst(BUILD_TUPLE, (values[oparg] -- tup)) {
tup = _PyTuple_FromArraySteal(values, oparg);
ERROR_IF(tup == NULL, error);
}
從指令名字就猜的出來是要建一個 Tuple,並且把 LOAD_CLOSURE
讀出來的值放進去。接著是 MAKE_FUNCTION 8
,這個我們在前面就有看過它,它是用來建立函數物件用的,但後面的 8
表示這個函數物件是個閉包:
// 檔案:Python/bytecodes.c
inst(MAKE_FUNCTION, (defaults if (oparg & 0x01),
kwdefaults if (oparg & 0x02),
annotations if (oparg & 0x04),
closure if (oparg & 0x08),
codeobj -- func)) {
// ... 略 ...
if (oparg & 0x08) {
assert(PyTuple_CheckExact(closure));
func_obj->func_closure = closure;
}
// ... 略 ...
}
因為 oparg
是 8,所以這裡會做的事就是把剛剛建立的 Tuple 放進去函數物件的 func_closure
成員裡,到這裡就算完成了 hey()
函數的建立了。接著再往下看看執行內層的 hey()
函數的時候發生什麼事。
0 COPY_FREE_VARS 1
6 4 LOAD_GLOBAL 1 (NULL + print)
14 LOAD_DEREF 0 (a)
16 CALL 1
24 POP_TOP
26 RETURN_CONST 0 (None)
這裡有個沒看過的指令 COPY_FREE_VARS 1
,這是在做什麼?
// 檔案:Python/bytecodes.c
inst(COPY_FREE_VARS, (--)) {
PyCodeObject *co = frame->f_code;
assert(PyFunction_Check(frame->f_funcobj));
PyObject *closure = ((PyFunctionObject *)frame->f_funcobj)->func_closure;
assert(oparg == co->co_nfreevars);
int offset = co->co_nlocalsplus - oparg;
for (int i = 0; i < oparg; ++i) {
PyObject *o = PyTuple_GET_ITEM(closure, i);
frame->localsplus[offset + i] = Py_NewRef(o);
}
}
這不太難理解,這裡的 closure
就是剛剛建立並且放在函數物件的 func_closure
成員的那個 Tuple,接著就是把這個 Tuple 裡的值複製到當前這個 Frame 的 localsplus
陣列裡,也就是變成這個 Frame 的區域變數,而且是接在原本的區域變數的後面。這樣在 hey()
函數裡面就可以使用外層函數的區域變數了。所以,所謂的「自由變數(Free Variable)」是指在內層函數本身沒有定義或宣告,而是使用外層函數的區域變數的情況,如果能看懂上面這個流程,自由變數看起來就一點都不神秘了。
前面我們都是從 CPython 的角度來看這些事情,如果想從 Python 的角度來看剛剛介紹的這些東西的話也是有方法的:
>>> hi.__code__.co_varnames
('b', 'hey')
>>> hi.__code__.co_cellvars
('a',)
每個函數都有 __code__
屬性,它會指向這個函數的 Code Object。在這個 Code Object 上面有 co_varnames
可以取得在這個函數裡定義的區域變數,可以看到目前只有 b
和 hey
。那變數 a
呢?它在 Python 的角度來看已經不是單純的區域變數了,而是透過 co_cellvars
來取得,表示它已經是一個 Cell Object 了。
是說,這些 co_
開頭的屬性是怎麼實作的?其實答案都在 PyCode_Type
結構的 tp_getset
跟 tp_members
成員裡:
// 檔案:Objects/codeobject.c
static PyGetSetDef code_getsetlist[] = {
{"co_lnotab", (getter)code_getlnotab, NULL, NULL},
{"_co_code_adaptive", (getter)code_getcodeadaptive, NULL, NULL},
{"co_varnames", (getter)code_getvarnames, NULL, NULL},
{"co_cellvars", (getter)code_getcellvars, NULL, NULL},
{"co_freevars", (getter)code_getfreevars, NULL, NULL},
{"co_code", (getter)code_getcode, NULL, NULL},
{0}
};
static PyMemberDef code_memberlist[] = {
{"co_argcount", T_INT, OFF(co_argcount), READONLY},
{"co_posonlyargcount", T_INT, OFF(co_posonlyargcount), READONLY},
{"co_kwonlyargcount", T_INT, OFF(co_kwonlyargcount), READONLY},
{"co_stacksize", T_INT, OFF(co_stacksize), READONLY},
{"co_flags", T_INT, OFF(co_flags), READONLY},
{"co_nlocals", T_INT, OFF(co_nlocals), READONLY},
{"co_consts", T_OBJECT, OFF(co_consts), READONLY},
{"co_names", T_OBJECT, OFF(co_names), READONLY},
{"co_filename", T_OBJECT, OFF(co_filename), READONLY},
{"co_name", T_OBJECT, OFF(co_name), READONLY},
{"co_qualname", T_OBJECT, OFF(co_qualname), READONLY},
{"co_firstlineno", T_INT, OFF(co_firstlineno), READONLY},
{"co_linetable", T_OBJECT, OFF(co_linetable), READONLY},
{"co_exceptiontable", T_OBJECT, OFF(co_exceptiontable), READONLY},
{NULL} /* Sentinel */
};
這些 co_
開頭的方法都在這裡了!這些方法會分成 tp_getset
以及 tp_members
的原因,是因為 tp_members
成員放的大多是比較靜態的屬性,這些屬性直接映射到結構裡的成員變數,它的記憶體指標的偏移值是固定的。讀取或修改這些屬性時比較不需要額外的計算,操作起來速度比較快。而 tp_getset
需要透過 getter 和 setter 函數來回傳或或修改指定的值,雖然比較有彈性,但就沒像像 tp_members
的操作那麼快。
如果想要看到更細部的操作,可以使用 Python 內建的中斷點(Breakpoint)來觀察:
def hi():
a = 1
b = 2
def hey():
print(a)
breakpoint()
執行之後進入互動模式,可以看到 hey
函數的的一些屬性:
$ python -i hi.py
>>> hi()
--Return--
> /Users/kaochenlong/projects/products/books/pythonbook.cc/hi.py(8)hi()->None
-> breakpoint()
(Pdb) hey
<function hi.<locals>.hey>
(Pdb) hey.__closure__
(<cell: int object>,)
(Pdb) hey.__closure__[0]
<cell: int object>
(Pdb) hey.__closure__[0].cell_contents
1
透過函數的 .__closure__
屬性可以取得內層 hey()
函數所有的 Cell,每個 Cell 都有一個 cell_contents
屬性,可以看到這顆 Cell 裡面包的是什麼東西。除此之外,Python 有個內建模組 inspect
,可以讓我們直接取得當前的 Frame:
(Pdb) import inspect
(Pdb) f = inspect.currentframe()
(Pdb) f
<frame>
(Pdb) f.f_code
<code object <module>>
(Pdb) f.f_locals
{
'b': 2,
'hey': <function hi.<locals>.hey>,
'a': 1,
'__return__': None,
'inspect': <module 'inspect'>,
'f': <frame>
}
(Pdb) f.f_locals['hey']
<function hi.<locals>.hey>
透過 inspect.currentframe()
函數可以取得當前的 Frame,之前我們在 Frame 看過的那些屬性就能透過它拿來玩看看了。關於 pdb
的使用方式,可參關「為你自己學 Python」的偵錯工具章節介紹。
不知道大家從一開始看到這個章節,是不是差不多已經能夠抓到假設想要知道某個功能是怎麼實作的,應該從什麼地方下手去追原始碼了呢 :)
本文同步刊載於 「為你自己學 Python - 虛擬機器五部曲(五)」